[笨叔点滴8] GNU C语言的扩展
开会迟到了,领导脸上有点绿,问:几点了
我说:3900点
领导:我问你什么时候
我:收盘的时候
领导怒了:出去!
我也急:长生了,全跌停,出。。出。。出不去”
”
昨天笨叔点滴7里给小伙伴留了小小的疑问?为啥子小明的程序像打了“真”的狂犬疫苗一样乱跳呢?有的朋友说,它没找到真正的栈,看下面这个图。
小明以为通过get_current_pt_regs()函数可以获取第一个进程init_task的栈,可是init_task这个变量,小明同学只是定义成全局的一个变量,它是存在data区的,但是小明天真的以为这个init_task分配了8KB的空间,实际上并没有,仅仅是初始化了一个struct task_struct结构体而已,它的实际大小也只有sizeof(task_struct)。所以通get_current_pt_regs()函数去找栈,那必定会改写了其他数据区的内容哟。
那在Linux内核中是怎么做的? (下面是内核的做法,当然现在内核是存放的thread_info的数据结构,但原理是类似的)
1. 在vmlinux.lds的链接脚本中data段里预留了8KB的空间给第一个进程用作内核栈。
2. 然后定义一个union,并且把 init_task这个变量 安装到这个".data..init_task"段里。
所以,就好比上面那张A股图,你以为“底”是在这里,其实真正的“底”是在那边。所以C语言时刻提醒我们需要关注内存情况,思考每个变量它究竟存在内存的什么地方,处处都是“雷”,和A股一样,很容易踩雷,那天就要被ST了。
今天我们和大家分享一下GNU C语言的一些扩展。
01 GCC 扩展
—
GCC的C编译器除了支持ANSI C标准之外,还对C语言进行了很多的扩充。这些扩充对代码优化、目标代码布局以及安全检查等方面提供了很强的支持,因此支持GNU扩展的C语言称为GNU C语言。Linux内核采用GCC编译器,所以Linux内核的代码自然使用了很多GCC的新扩充特性。本章介绍一些GCC的C语言扩充的新特性,希望读者在学习Linux内核的时候需要特别留意和关注。
1) 语句表达式
在GNU C语言中,括号里的复合语句可以看作是一个表达式,称为语句表达式。在一个语句表达式里,可以使用循环、跳转和局部变量等。这个特性通常用在宏定义中,可以让宏定义变的更安全,如比较两个值的大小。
#define max(a,b) ((a) >(b) ? (a) : (b))
上述代码会安全问题,a和b有可能会计算两次,比如a传入i++,b传入j++。在GNU C中,如果知道a和b的类型,可以这样写这个宏。
#define maxint(a,b) \
({int _a = (a), _b = (b); _a > _b ? _a :_b; })
如果你不知道a和b的类型,还可以使用typeof类转换宏。
<include/linux/kernel.h>
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2; })
typeof也是GNU C语言的一个扩充用法,可以用来构造新的类型,通常和语句表达式一起使用。下面是一些例子。
typeof (*x) y;
typeof (*x) z[4];
typeof (typeof (char *)[4])m;
第一句是声明y是x指针指向的类型。第二句声明z是一个数组,其中数组的类型是x指针指向的类型。第三句声明m是一个指针数组,和“char *m[4]”声明是一样的。
2) 零长数组
GNU C允许使用变长数组,在定义数据结构的时候非常有用。
<mm/percpu.c>
struct pcpu_chunk {
struct list_head list; /* linked to pcpu_slot lists */
unsigned long populated[]; /* populated bitmap */
};
数据结构最后一个元素定义为零长度数组,不占结构体空间。这样,我们可以根据对象大小动态的去分配结构的大小。
struct line {
int length;
char contents[0];
};
struct line *thisline =malloc(sizeof(struct line) + this_length);
thisline->length =this_length;
如上述例子,structline数据结构定义了一个intlength变量和一个变长数组contents[0],这个struct line数据结构的大小只包含int类型的大小,不包含contents的大小,也就是sizeof (struct line) = sizeof(int)。创建结构体对象时,可根据实际的需要指定这个可变长数组的长度,并分配相应的空间,如上述实例代码分配了this_length个字节的内存,并且可以通过contents[index]来访问第index个地址的数据。
3) case范围
GNU C支持指定一个case的范围作为一个标签,如:
case low ... high:
case 'A' ... 'Z':
这里low到high表示一个区间范围,另外在ASCII字符代码也非常有用,下面是Linux内核中的代码例子。
<arch/x86/platform/uv/tlb_uv.c>
static int local_atoi(constchar *name)
{
int val = 0;
for (;; name++) {
switch (*name) {
case '0' ... '9':
val = 10*val+(*name-'0');
break;
default:
return val;
}
}
}
另外还可以用整形数来表示范围,但是这里需要注意在“...”两边需要有空格,否则编译出错。
<drivers/usb/gadget/udc/at91_udc.c>
static intat91sam9261_udc_init(struct at91_udc *udc)
{
for (i = 0; i < NUM_ENDPOINTS; i++) {
ep = &udc->ep[i];
switch (i) {
case 0:
ep->maxpacket = 8;
break;
case 1 ... 3:
ep->maxpacket = 64;
break;
case 4 ... 5:
ep->maxpacket = 256;
break;
}
}
}
4) 标号元素
标准C语言要求数组或结构体初始化值必须以固定顺序出现,但在GNU C语言中,可以通过指定索引或结构体成员名来初始化,不必按照原来的固定顺序进行初始化。
结构体成员的初始化在Linux内核中经常使用,如在设备驱动中初始化file_operations数据结构,下面是Linux内核中的一个代码例子。
<drivers/char/mem.c>
static const structfile_operations zero_fops = {
.llseek = zero_lseek,
.read =new_sync_read,
.write = write_zero,
.read_iter =read_iter_zero,
.aio_write =aio_write_zero,
.mmap = mmap_zero,
};
如上述代码中的zero_fops的成员llseek初始化为zero_lseek函数,read成员初始化为new_sync_read函数,依次类推。当file_operations数据结构的定义发生变化时,这种初始化方法依然能保证已知元素的正确性,对于未初始化成员的值为0或者NULL。
未完待续。。。
[往期精彩]
[笨叔点滴1] 为什么do_page_fault函数里代码需要判断用户态还是内核态?
[笨叔点滴2] 为啥子ARM32体系结构中每个处理模式都有一个单独的栈?
[笨叔点滴5] git rebase和git merge究竟有啥区别?